﻿/*
TouchFramework connects touch tracking from a tracking engine to WPF controls 
allow scaling, rotation, movement and other multi-touch behaviours.

Copyright 2009 - Mindstorm Limited (reg. 05071596)

Author - Julien Vulliet

This file is part of TouchFramework.

TouchFramework is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

TouchFramework is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser Public License for more details.

You should have received a copy of the GNU Lesser Public License
along with TouchFramework.  If not, see <http://www.gnu.org/licenses/>.

If you have any questions regarding this library, or would like to purchase 
a commercial licence, please contact Mindstorm via www.mindstorm.com.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Net.Sockets;
using System.Threading;
using System.Net;
using TouchFramework;
using TouchFramework.Config;
using TouchFramework.Tracking;
using TouchFramework.Tracking.Tuio;
using System.Drawing;

namespace TouchFrameworkTracking.TUIO
{
    /// <summary>
    /// Tracking implementation for receiving TUIO data and acting upon the events with WPF.
    /// </summary>
    public class TuioTracking : FrameworkControl
    {
        #region Fields
        TuioConfiguration _config;
        UdpClient _udpreceiver;
        Thread _thr;
        bool _isrunning;
        readonly Dictionary<int, Tuio2DCursor> _current = new Dictionary<int, Tuio2DCursor>();
        readonly object _mLock = new object();
        #endregion

        #region Properties
        public double ScreenWidth = SystemParameters.PrimaryScreenWidth;
        public double ScreenHeight = SystemParameters.PrimaryScreenHeight;
        #endregion

        public TuioTracking(FrameworkElement uiParent)
        {
            Owner = uiParent;
        }

        public override void ConfigureFramework(FrameworkConfiguration config)
        {
            if (!(config is TuioConfiguration)) throw new ArgumentException("Config must be TuioConfiguration");

            _config = config as TuioConfiguration;
                
            base.ConfigureFramework(config);
        }

        public override void Start()
        {
            if (_isrunning) return;

            var endpoint = new IPEndPoint(IPAddress.Any, _config.Port);
            _udpreceiver = new UdpClient(endpoint);

            _isrunning = true;
            _thr = new Thread(Receive);
            _thr.Start();
        }

        public override void Stop()
        {
            if (!_isrunning) return;

            _isrunning = false;
            Close();
        }

        void Close()
        {
            try
            {
                //Might throw an exception which is meaningless when we are shutting down
                _udpreceiver.Close();
            }
            catch
            {

            }
        }

        public override void ForceRefresh()
        {
            // For tuio this is only useful to remove stuck points after a TUIO server restart
            lock (_mLock)
            {
                _current.Clear();
            }
        }

        void Receive()
        {
            try
            {
                ReceiveData();
            }
            catch
            {
                // Ignore errors for now
            }
            finally
            {
                // Try to stop cleanly on termination of the blocking receivedata function
                Stop();
            }
        }

        void ReceiveData()
        {
            try
            {
                while (_isrunning)
                {
                    IPEndPoint ip = null;

                    var buffer = _udpreceiver.Receive(ref ip);
                    var bundle = OSCPacket.Unpack(buffer) as OSCBundle;

                    if (bundle == null) continue;

                    var alivecursors = TuioParser.GetAliveCursors(bundle);
                    var newcursors = TuioParser.GetCursors(bundle);
                        
                    RemoveNotAlive(alivecursors);
                    UpdateSetCursors(newcursors, alivecursors);
                    AddNewCursors(newcursors);
                        
                    ProcessUpdates();
                }
            }
            catch
            {
            }
        }

        void AddNewCursors(Dictionary<int, Tuio2DCursor> sets)
        {
            var result = (from entry in sets
                          where (!_current.ContainsKey(entry.Key))
                          select entry.Value);

            foreach (var cur in result)
                TouchAdded(cur);
        }

        void UpdateSetCursors(IDictionary<int, Tuio2DCursor> sets, IEnumerable<int> alive)
        {
            foreach (var curid in alive)
            {
                //Held cursor
                if (!sets.ContainsKey(curid) && _current.ContainsKey(curid))
                {
                    TouchHeld(curid);
                }
                else
                {
                    if (sets.ContainsKey(curid) && _current.ContainsKey(curid))
                    {
                        Tuio2DCursor cur = sets[curid];
                        if (cur.IsEqual(_current[curid]))
                        {
                            //Call touchheld if same value
                            TouchHeld(curid);
                        }
                        else
                        {
                            TouchUpdated(cur);
                        }
                    }
                }
            }
        }

        void RemoveNotAlive(List<int> alive)
        {
            // Get all the ones to delete
            var result = (from entry in _current
                          where (!alive.Contains(entry.Key))
                          select entry.Key).ToArray<int>();

            // Delete them
            foreach (var i in result)
                TouchRemoved(i);
        }

        protected void TouchUpdated(Tuio2DCursor cur)
        {
            _current[cur.SessionID] = cur;
            var t = BuildTouch(cur);
            TouchUpdated(t.TouchId, t.TouchPoint, t.Properties);
        }

        protected void TouchAdded(Tuio2DCursor cur)
        {
            _current.Add(cur.SessionID, cur);
            var t = BuildTouch(cur);
            TouchAdded(t);
        }

        protected new void TouchRemoved(int touchId)
        {
            _current.Remove(touchId);
            base.TouchRemoved(touchId);
        }

        Touch BuildTouch(Tuio2DCursor cursor)
        {
            var prop=new TouchProperties
                         {
                             Acceleration = cursor.Acceleration,
                             VelocityX = cursor.VelocityX,
                             VelocityY = cursor.VelocityY
                         };

            var p = GetTouchPoint(cursor);
            var t = new Touch(cursor.SessionID, p) {Properties = prop};
            return t;
        }

        PointF GetTouchPoint(Tuio2DCursor data)
        {
            var x1 = GetScreenPoint(data.PositionX,
                _config.Projection.ScaleX,
                _config.Projection.OffsetX,
                _config.Alignment.FlipX,
                _config.CorrectProjection,
                ScreenWidth);
            var y1 = GetScreenPoint(data.PositionY,
                _config.Projection.ScaleY,
                _config.Projection.OffsetY,
                _config.Alignment.FlipY,
                _config.CorrectProjection,
                ScreenHeight);

            var t = new PointF(x1, y1);
            return t;
        }

        static float GetScreenPoint(float xOrY, float scale, float offset, bool flip, bool correctProjection, double screenDimension)
        {
            if (flip) 
                xOrY = PointTransform.Flip(xOrY, 0.5f);
            
            var projSpace = xOrY * 2f - 1f;
            if (correctProjection) projSpace = PointTransform.ScaleOffset(projSpace, scale, offset);
            xOrY = PointTransform.ScaleZeroToOne(projSpace);
            xOrY *= (float)screenDimension;
            return xOrY;
        }
    }
}
